/*
 * Copyright (C) 2016 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package okhttp3.slack;

import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.Map;

import mockwebserver3.Dispatcher;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import okhttp3.HttpUrl;

import okio.ByteString;

/**
 * Runs a MockWebServer on localhost and uses it as the backend to receive an OAuth session.
 *
 * <p>Clients should call {@link #start}, {@link #newAuthorizeUrl} and {@link #close} in that order.
 * Clients may request multiple sessions.
 */
public final class OAuthSessionFactory extends Dispatcher implements Closeable {
    private final SecureRandom secureRandom = new SecureRandom();

    private final SlackApi slackApi;
    private MockWebServer mockWebServer;

    /**
     * Guarded by this.
     */
    private Map<ByteString, Listener> listeners = new LinkedHashMap<>();

    public OAuthSessionFactory(SlackApi slackApi) {
        this.slackApi = slackApi;
    }

    public void start() throws Exception {
        if (mockWebServer != null) throw new IllegalStateException();

        mockWebServer = new MockWebServer();
        mockWebServer.setDispatcher(this);
        mockWebServer.start(slackApi.port);
    }

    public HttpUrl newAuthorizeUrl(String scopes, String team, Listener listener) {
        if (mockWebServer == null) throw new IllegalStateException();

        ByteString state = randomToken();
        synchronized (this) {
            listeners.put(state, listener);
        }

        return slackApi.authorizeUrl(scopes, redirectUrl(), state, team);
    }

    private ByteString randomToken() {
        byte[] bytes = new byte[16];
        secureRandom.nextBytes(bytes);
        return ByteString.of(bytes);
    }

    private HttpUrl redirectUrl() {
        return mockWebServer.url("/oauth/");
    }

    @Override
    public MockResponse dispatch(RecordedRequest request) {
        HttpUrl requestUrl = mockWebServer.url(request.getPath());
        String code = requestUrl.queryParameter("code");
        String stateString = requestUrl.queryParameter("state");
        ByteString state = stateString != null ? ByteString.decodeBase64(stateString) : null;

        Listener listener;
        synchronized (this) {
            listener = listeners.get(state);
        }

        if (code == null || listener == null) {
            return new MockResponse()
                    .setResponseCode(404)
                    .setBody("unexpected request");
        }

        try {
            OAuthSession session = slackApi.exchangeCode(code, redirectUrl());
            listener.sessionGranted(session);
        } catch (IOException e) {
            return new MockResponse()
                    .setResponseCode(400)
                    .setBody("code exchange failed: " + e.getMessage());
        }

        synchronized (this) {
            listeners.remove(state);
        }

        // Success!
        return new MockResponse()
                .setResponseCode(302)
                .addHeader("Location", "https://twitter.com/CuteEmergency/status/789457462864863232");
    }

    public interface Listener {
        void sessionGranted(OAuthSession session);
    }

    @Override
    public void close() {
        if (mockWebServer == null) throw new IllegalStateException();
        try {
            mockWebServer.close();
        } catch (IOException ignored) {
        }
    }
}
